/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.commands;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.handlers.events.EventListenerBinder;
import com.hercules.android.receiver.handlers.events.EventListenerBindingException;
import com.hercules.android.receiver.model.commands.XRECommand;
import com.hercules.android.receiver.model.objects.XRECommandSequence;
import com.hercules.android.receiver.model.objects.XREEventHandler;
import com.hercules.android.receiver.model.objects.XREObject;
import com.hercules.android.receiver.model.objects.XREStreamResource;
import com.hercules.android.receiver.model.objects.XREView;
import com.hercules.android.receiver.painting.XREObjectPainter;
import com.hercules.android.receiver.painting.XREObjectPainterException;
import com.hercules.android.receiver.painting.views.AndroidXREView;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;

/**
 * Helper class for this component.
 * <p>
 * AndroidAssembly1 changed name and some methods
 * <p>
 * <b>Thread safety:</b> This class is immutable and so thread safe.
 *
 * @author 7realm
 * @author 7realm
 * @version 1.0
 */
@SuppressWarnings("unchecked")
final class CommandHandlersUtil {
    /** Represents maximum value for {@link XREStreamResource}. */
    static final int MAX_VOLUME = 100;
    /** Represents dimensions of matrix parameter value. */
    private static final int DIMENSIONS = 4;

    /**
     * This private constructor prevents to create a new instance.
     */
    private CommandHandlersUtil() {
        // empty
    }

    /**
     * Checks whether the given {@link Object} is null.
     *
     * @param arg the argument to check
     * @param name the name of the argument to check
     * @throws IllegalStateException if the given object is null
     */
    static void checkState(Object arg, String name) {
        if (arg == null) {
            throw new IllegalStateException("Value of '" + name + "' should not be null.");
        }
    }

    /**
     * Checks whether the given {@link Object} is null.
     *
     * @param arg the argument to check
     * @param name the name of the argument to check
     * @throws IllegalArgumentException if the given object is null
     */
    static void checkNotNull(Object arg, String name) {
        if (arg == null) {
            throw new IllegalArgumentException("Argument '" + name + "' value should not be null.");
        }
    }

    /**
     * Checks if receiver is correct.
     *
     * @param receiver the current receiver to check
     * @throws IllegalArgumentException if receiver is null
     * @throws ValidationException if receiver is not correct
     */
    static void checkReceiver(AndroidReceiver receiver) throws ValidationException {
        checkNotNull(receiver, "receiver");
        if (receiver.getApplicationData() == null) {
            throw new ValidationException("Value of 'receiver.applicationData' should not be null.");
        }
        if (receiver.getApplicationData().getElementsXREIdToObject() == null) {
            throw new ValidationException("Value of "
                + "'receiver.applicationData.elementsXREIdToObject' should not be null.");
        }
    }

    /**
     * Checks if command has correct properties.
     *
     * @param <T> the type of expected command
     * @param command the command to check
     * @param expectedCommandClass expected command class
     * @return command, casted to expected type
     * @throws IllegalArgumentException if command is null
     * @throws ValidationException if command type is not correct
     */
    static <T extends XRECommand> T checkCommand(XRECommand command, Class<T> expectedCommandClass)
        throws ValidationException {
        checkNotNull(command, "command");
        if (!expectedCommandClass.isInstance(command)) {
            throw new ValidationException("Passed command of type '" + command.getClass().getName()
                + "' but expected command of type '" + expectedCommandClass.getName() + "'.");
        }

        return (T) command;
    }

    /**
     * Checks string command parameter.
     *
     * @param name the name of parameter
     * @param value the value of parameter
     * @throws InvalidXRECommandParameterException if parameter value is null or empty
     */
    static void checkStringParameter(String name, String value) throws InvalidXRECommandParameterException {
        if (value == null) {
            throw new InvalidXRECommandParameterException(
                "The '" + name + "' parameter of command should not be null.", name, value);
        }
        if (value.trim().length() == 0) {
            throw new InvalidXRECommandParameterException("The '" + name
                + "' parameter of command should not be trimmed empty.", name, value);
        }
    }

    /**
     * Checks map.
     *
     * @param map the map to check
     * @param name the name of the map to check
     * @throws IllegalArgumentException if map is invalid
     */
    static void checkMap(Map<String, ?> map, String name) {
        checkNotNull(map, name);
        if (map.isEmpty()) {
            throw new IllegalArgumentException("Argument '" + name + "' should not be empty.");
        }
        for (Entry<String, ?> entry : map.entrySet()) {
            checkString(entry.getKey(), name + ".key");
            checkNotNull(entry.getValue(), name + ".value");
        }
    }

    /**
     * Checks collection.
     *
     * @param collection the collection to check
     * @param expectedClass the expected class of collection items
     * @param paramName the current parameter name
     * @param paramValue the current parameter value
     * @param collectionName the name of collection
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     */
    static void checkCollection(Collection<?> collection, Class<?> expectedClass, String paramName, Object paramValue,
        String collectionName) throws InvalidXRECommandParameterException {
        for (Object item : collection) {
            if (!expectedClass.isInstance(item)) {
                throw new InvalidXRECommandParameterException("Collection '" + collectionName
                    + "' must have items of '" + expectedClass.getName() + "' type.", paramName, paramValue);
            }
        }
    }

    /**
     * Setups object's event handlers.
     * <p>
     * AndroidAssembly1 added arguments to support binding of events
     *
     * @param logger the logger
     * @param clazz the calling class name
     * @param methodName the calling method name
     * @param xreObject the created object
     * @param params the command parameters
     * @param elementsMap the ApplicationData map of XREObjects
     * @param receiver the android receiver
     * @param binder the event binder
     * @throws XREObjectNotFoundException if any XREObject to be associated is not found
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     * @throws UnsupportedXRECommandParameterException if there is unsupported command parameter
     */
    @SuppressWarnings("boxing")
    static void setupHandlers(Logger logger, Object clazz, String methodName, XREObject xreObject,
        Map<String, Object> params, Map<Integer, XREObject> elementsMap, AndroidReceiver receiver,
        EventListenerBinder binder) throws InvalidXRECommandParameterException, XREObjectNotFoundException,
        UnsupportedXRECommandParameterException {
        Iterator<Entry<String, Object>> i = params.entrySet().iterator();
        while (i.hasNext()) {
            Entry<String, Object> entry = i.next();
            // skip not events
            String key = entry.getKey();
            if (key == null || !key.startsWith("on")) {
                continue;
            }
            Object value = entry.getValue();
            if (value instanceof XREEventHandler) {
                xreObject.getEventHandlers().put(key, (XREEventHandler) value);
            } else if (value == null) {
                // remove handler
                if (xreObject.getEventHandlers().remove(key) == null && logger != null) {
                    LogUtil.warn(logger, clazz, methodName,
                        "There are no event handler with key '{0}', but it is removed.", key);
                }
            } else {
                createEventHandler(xreObject, elementsMap, key, value);
/*
                // AndroidAssembly1 added XRE view event binding
                if (xreObject instanceof XREView) {
                    XREView xreView = (XREView) xreObject;

                    // get corresponding view
                    AndroidXREView view = receiver.getApplicationData().getViewsXREIdToView().get(xreView.getId());

                    // log event
                    LogUtil.info(logger, clazz, methodName, "Binding XREView event {0} to {1}.", key, xreView.getId());

                    try {
                        if (key.endsWith("MouseDown") || key.endsWith("MouseUp") || key.endsWith("MouseWheel")) {
                            binder.bindMouseEventListener(key.replace("Preview", ""), xreView, view, receiver);
                        } else if (key.endsWith("KeyUp") || key.endsWith("KeyDown")) {
                            binder.bindKeyEventListener(key, xreView, view, receiver);
                        } else if (XREActivateViewEvent.ON_ACTIVATE_EVENT_NAME.equals(key)
                            || XREActivateViewEvent.ON_DEACTIVATE_EVENT_NAME.equals(key)) {
                            binder.bindViewActivationEventListener(key.replace("Preview", ""), xreView, view, receiver);
                        } else {
                            LogUtil.warn(logger, clazz, methodName, "XREView event {0} is not binded to {1}.", key,
                                xreView.getId());
                        }
                    } catch (EventListenerBindingException e) {
                        throw new UnsupportedXRECommandParameterException("Failed to bind '" + key
                            + "' event to XREView '" + xreView.getId() + "'.", e, key);
                    }
                }*/
            }

            i.remove();
        }
        
        if (xreObject instanceof XREView) {
        	XREView xreView = (XREView) xreObject;

            // get corresponding view
            AndroidXREView view = receiver.getApplicationData().getViewsXREIdToView().get(xreView.getId());
            
            if (view != null && binder != null) {
	            try {
	                binder.bindMouseEventListener("MouseEvent", xreView, view, receiver);
	                //binder.bindKeyEventListener("KeyEvent", xreView, view, receiver);
	                binder.bindViewActivationEventListener("ViewActivationEvent", xreView, view, receiver);
	            } catch (EventListenerBindingException e) {
	                throw new UnsupportedXRECommandParameterException("Failed to bind event to XREView '" + xreView.getId() + "'.", e, "");
	            }
            }
        }
    }

    /**
     * Gets value of parameter.
     *
     * @param <T> the type of returned value
     * @param params the command parameters
     * @param name the name of command parameter
     * @param expectedClass the expected class of returned value
     * @param isRequired true if this value is required
     * @return the parameter value
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     */
    static <T> T getValue(Map<String, Object> params, String name, Class<T> expectedClass, boolean isRequired)
        throws InvalidXRECommandParameterException {
        if (params.containsKey(name)) {
            // remove parameter from temp map
            Object value = params.remove(name);
            if (value == null) {
                // AndroidAssembly1 allowed null value return
                return null;
            }
            if (expectedClass.isInstance(value)) {
                return (T) value;
            }
            throw new InvalidXRECommandParameterException("Value of '" + name + "' parameter is not of '"
                + expectedClass.getName() + "' type.", name, value);
        }

        // parameter is missing
        if (isRequired) {
            throw new InvalidXRECommandParameterException("Required parameter '" + name + "' is missing.", name, null);
        }
        return null;
    }

    /**
     * Gets value of parameter.
     *
     * @param <T> the type of returned value
     * @param params the command parameters
     * @param name the name of command parameter
     * @param expectedClass the expected class of returned value
     * @param defaultValue the default value
     * @return value of optional parameter
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     */
    static <T> T getValue(Map<String, Object> params, String name, Class<T> expectedClass, T defaultValue)
        throws InvalidXRECommandParameterException {
        T value = getValue(params, name, expectedClass, false);
        return value == null ? defaultValue : value;
    }

    /**
     * Gets object by ID.
     *
     * @param <T> the type of returned object
     * @param params the command parameters
     * @param idParamName the name of id parameter
     * @param elementsMap the ApplicationData map of XREObjects
     * @param expectedClass the expected class of required object
     * @param defaultValue the default value
     * @return the required object
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     * @throws XREObjectNotFoundException if any XREObject to be associated is not found
     */
    static <T extends XREObject> T getById(Map<String, Object> params, String idParamName,
        Map<Integer, XREObject> elementsMap, Class<T> expectedClass, T defaultValue)
        throws InvalidXRECommandParameterException, XREObjectNotFoundException {
        Integer id = getValue(params, idParamName, Integer.class, false);
        if (id == null) {
            // AndroidAssembly1 added default value return on null value
            return defaultValue;
        }
        return getById(elementsMap, expectedClass, id, idParamName);
    }

    /**
     * Gets string parameter value.
     *
     * @param params the command parameters
     * @param name the name of command parameter
     * @param isRequired true if this value is required
     * @param allowEmpty if empty strings are allowed
     * @param defaultValue the default value
     * @return the string parameter value
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     */
    static String getString(Map<String, Object> params, String name, boolean isRequired, boolean allowEmpty,
        String defaultValue) throws InvalidXRECommandParameterException {
        String value = getValue(params, name, String.class, isRequired);
        if (!isRequired && value == null) {
            return defaultValue;
        }
        if (!allowEmpty && value.trim().length() == 0) {
            throw new InvalidXRECommandParameterException("String value of '" + name + "' is empty.", name, value);
        }
        return value;
    }

    /**
     * Gets number parameter value.
     *
     * @param <T> the type of number, int or double is used
     * @param params the command parameters
     * @param name the name of command parameter
     * @param isRequired true if this value is required
     * @param positive true if parameter value is required to be positive
     * @param maxValue the maximum parameter value
     * @param defaultValue the default value
     * @return the number parameter value
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     */
    static <T extends Number> T getNum(Map<String, Object> params, String name, boolean isRequired, boolean positive,
        T maxValue, T defaultValue) throws InvalidXRECommandParameterException {
        T value = (T) getValue(params, name, defaultValue.getClass(), isRequired);
        if (!isRequired && value == null) {
            return defaultValue;
        }
        if (positive && value.doubleValue() < 0) {
            throw new InvalidXRECommandParameterException("Value of '" + name + "' is less than 0.", name, value);
        }
        if (positive && maxValue.intValue() != -1 && value.doubleValue() > maxValue.doubleValue()) {
            throw new InvalidXRECommandParameterException(
                "Value of '" + name + "' is greater than '" + maxValue + "'.", name, value);
        }
        return value;
    }

    /**
     * Gets boolean parameter value.
     *
     * @param params the command parameters
     * @param name the name of command parameter
     * @param isRequired true if this value is required
     * @param defaultValue the default value
     * @return the boolean parameter value
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     */
    @SuppressWarnings("boxing")
    static boolean getBool(Map<String, Object> params, String name, boolean isRequired, boolean defaultValue)
        throws InvalidXRECommandParameterException {
        Boolean value = getValue(params, name, Boolean.class, isRequired);
        if (!isRequired && value == null) {
            return defaultValue;
        }
        return value;
    }

    /**
     * Gets dimensions parameter value.
     *
     * @param params the command parameters
     * @param defaultValue default value
     * @return the value of dimensions parameter
     * @throws InvalidXRECommandParameterException if parameter value is invalid
     */
    static int[] getDimensions(Map<String, Object> params, int[] defaultValue)
        throws InvalidXRECommandParameterException {
        int[] dimensions = getValue(params, "dimensions", int[].class, false);
        if (dimensions == null) {
            dimensions = defaultValue;
        } else if (dimensions.length != 2) {
            throw new InvalidXRECommandParameterException("Length of 'dimensions' is not 2.", "dimensions", dimensions);
        } else {
            for (int i = 0; i < dimensions.length; i++) {
                if (dimensions[i] < 0) {
                    throw new InvalidXRECommandParameterException("Value of dimensions[" + i + "] is negative.",
                        "dimensions", dimensions);
                }
            }
        }
        return dimensions;
    }

    /**
     * Gets matrix parameter value.
     *
     * @param params the command parameters
     * @param defaultValue default value
     * @return the value of matrix parameter
     * @throws InvalidXRECommandParameterException if parameter value is invalid
     */
    static double[][] getMatrix(Map<String, Object> params, double[][] defaultValue)
        throws InvalidXRECommandParameterException {
        double[][] matrix = getValue(params, "matrix", double[][].class, false);
        if (matrix == null) {
            matrix = defaultValue;
        } else if (matrix.length != DIMENSIONS) {
            throw new InvalidXRECommandParameterException("Value of 'matrix' is not 4x4.", "matrix", matrix);
        } else {
            for (int i = 0; i < DIMENSIONS; i++) {
                if (matrix[i].length != DIMENSIONS) {
                    throw new InvalidXRECommandParameterException("Value of 'matrix' is not 4x4.", "matrix", matrix);
                }
            }
        }
        return matrix;
    }

    /**
     * Checks if given parameter map is empty.
     * <p>
     * AndroidAssembly1 warning instead of exception
     *
     * @param params the map to check
     * @param name the name of the map
     * @param logger the current logger
     * @param clazz the calling instance
     * @param methodName the calling method
     */
    static void checkParams(Map<String, ?> params, String name, Logger logger, Object clazz, String methodName) {
        Iterator<String> i = params.keySet().iterator();
        while (i.hasNext()) {
            String param = i.next();

            // skip null keys
            if (param != null) {
                LogUtil.warn(logger, clazz, methodName, "Command property {0} contains unsupported parameter {1}.",
                    name, param);
            }
        }
    }

    /**
     * Checks whether the given {@link String} is null or empty (the length of the given string is zero after it is
     * trimmed).
     *
     * @param arg the String to check
     * @param name the name of the String argument to check
     * @throws IllegalArgumentException if the given string is null or empty
     */
    private static void checkString(String arg, String name) {
        checkNotNull(arg, name);
        if (arg.trim().length() == 0) {
            throw new IllegalArgumentException("Argument '" + name + "' value should not be empty string.");
        }
    }

    /**
     * Gets object by ID.
     *
     * @param elementsMap the ApplicationData map of XREObjects
     * @param expectedClass the expected class of required object
     * @param id the id of required object
     * @param paramName the name of parameter that has ID
     * @param <T> the type of returned object
     * @return the required object
     * @throws InvalidXRECommandParameterException if any command parameter has invalid value
     * @throws XREObjectNotFoundException if any XREObject to be associated is not found
     */
    @SuppressWarnings("boxing")
    private static <T> T getById(Map<Integer, XREObject> elementsMap, Class<T> expectedClass, Integer id,
        String paramName) throws InvalidXRECommandParameterException, XREObjectNotFoundException {
        if (id.equals(XREObject.ID_NULL)) {
            return null;
        }
        if (elementsMap.containsKey(id)) {
            XREObject xreObject = elementsMap.get(id);
            if (expectedClass.isInstance(xreObject)) {
                return (T) xreObject;
            }
            throw new InvalidXRECommandParameterException("Object with id '" + id + "' is not of '"
                + expectedClass.getName() + "' type.", paramName, id);
        }
        throw new XREObjectNotFoundException("Command parameter '" + paramName + "' contains ID '" + id
            + "' of object that does not exist.", id);
    }

    /**
     * Create event handler.
     *
     * @param xreObject the owner of event handlers
     * @param elementsMap the application data elements map
     * @param key the event handler name
     * @param value the value, used to create event handler
     * @throws InvalidXRECommandParameterException if any command parameter is invalid
     * @throws XREObjectNotFoundException if object is not found by given ID
     * @throws UnsupportedXRECommandParameterException if there is unsupported command parameter
     */
    private static void createEventHandler(XREObject xreObject, Map<Integer, XREObject> elementsMap, String key,
        Object value) throws InvalidXRECommandParameterException, XREObjectNotFoundException,
        UnsupportedXRECommandParameterException {
        XREEventHandler eventHandler = new XREEventHandler();
        if (value instanceof XRECommandSequence) {
            // put command sequence to new event handler
            eventHandler.setCommandSequence((XRECommandSequence) value);
        } else if (value instanceof Integer) {
            // get sequence by id
            XRECommandSequence sequence = getById(elementsMap, XRECommandSequence.class, (Integer) value, key);

            // put sequence to new event handler
            eventHandler.setCommandSequence(sequence);
            xreObject.getEventHandlers().put(key, eventHandler);
        } else if (value instanceof Map<?, ?>) {
            // check collection
            Map<?, ?> subMap = (Map<?, ?>) value;
            checkCollection(subMap.keySet(), String.class, key, value, key + ".keys");

            // set filter if it is present
            if (subMap.containsKey("filter")) {
                Object filter = subMap.get("filter");
                if (filter instanceof List<?>) {
                    List<?> list = (List<?>) filter;
                    checkCollection(list, Map.class, key, value, key + ".filter");
                    for (Object object : list) {
                        checkCollection(((Map<?, ?>) object).keySet(), String.class, key, value, key + ".filter.keys");
                    }
                } else {
                    throw new InvalidXRECommandParameterException("Key '" + key
                        + ".filter' must have List value for event handler.", key, value);
                }

                eventHandler.setFilter((List<Map<String, Object>>) filter);
                subMap.remove("filter");
            }

            // set command sequence if it is present
            if (subMap.containsKey("commandSequence")) {
                Object commandSequence = subMap.get("commandSequence");
                if (commandSequence instanceof Integer) {
                    eventHandler.setCommandSequence(getById(elementsMap, XRECommandSequence.class,
                        (Integer) commandSequence, key));
                } else if (commandSequence instanceof XRECommandSequence) {
                    eventHandler.setCommandSequence((XRECommandSequence) commandSequence);
                } else {
                    throw new InvalidXRECommandParameterException("Key 'commandSequence' must have int or "
                        + "XRECommandSequence value for event handler.", key, value);
                }
                subMap.remove("commandSequence");
            }

            if (subMap.size() != 0) {
                throw new UnsupportedXRECommandParameterException("Map in '" + key
                    + "' can have only 'filter' and 'commandSequence' keys.", key);
            }
        } else {
            throw new InvalidXRECommandParameterException("Not supported value for event handler key.", key, value);
        }
        xreObject.getEventHandlers().put(key, eventHandler);
    }

    /**
     * Helper exception, used for validation errors.
     *
     * @author 7realm
     * @version 1.0
     */
    static class ValidationException extends Exception {
        /** Serial version UID. */
        private static final long serialVersionUID = -7521545460815958378L;

        /**
         * Constructor for {@link ValidationException} with error message.
         *
         * @param message the error message
         */
        public ValidationException(String message) {
            super(message);
        }
    }

    /**
     * Paint root view.
     *
     * @param command the current XRECommand
     * @param receiver the current AndroidReceiver
     * @param applicationPainter the application painter
     * @throws XRECommandHandlerException if root view is not present or has incorrect type
     * @throws XREObjectPainterException if error occurs while painting root view
     */
    @SuppressWarnings("boxing")
    static void paintRootView(XRECommand command, AndroidReceiver receiver, XREObjectPainter applicationPainter)
        throws XRECommandHandlerException, XREObjectPainterException {
        // paint root view
        if (applicationPainter != null) {
            // get the rootView
            XREObject xreObject = receiver.getApplicationData().getElementsXREIdToObject().get(XREObject.ID_ROOT_VIEW);
            if (xreObject == null) {
                throw new XRECommandHandlerException(
                    "Root view is absent in receiver.applicationData.elementsXREIdToObject.");
            }
            if (xreObject instanceof XREView) {
                // paint root view
                applicationPainter.paintView(receiver, (XREView) xreObject, command);
            } else {
                throw new XRECommandHandlerException("Object associated with root view ID is not a XREView.");
            }
        }
    }
}
